﻿// <copyright file="BufferGeometryList.cs" company="Signal Processing Lab, Wuhan University">
// Copyright (c) 2009-2010 All Right Reserved
// </copyright>
// <author>Xinping Deng</author>
// <email>dentoby@gmail.com</email>
// <date>2010-4</date>
// <summary>This is part of Image.NET project.</summary>

namespace SPLWHU.Core.Vectors
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.IO;
    using OGR = OSGeo.OGR;

    /// <summary>
    /// The OGR shape provider.
    /// </summary>
    /// <typeparam name="T">The geometry type.</typeparam>
    public class OgrShapeProvider<T> : IShapeProvider where T : Geometry, new()
    {
        #region Private Fields

        /// <summary>
        /// The prefix of file name.
        /// </summary>
        private static readonly string FilenamePrefix = "OGR_CACHE_";

        /// <summary>
        /// The maximun count of geometries allowed to delete before flushing the data set.
        /// </summary>
        private static readonly int MaxDeleteCount = 50;

        /// <summary>
        /// The fids of deleted geometries.
        /// </summary>
        private List<int> deleteFid = new List<int>(OgrShapeProvider<T>.MaxDeleteCount);

        /// <summary>
        /// The layer name.
        /// </summary>
        private string layerName;

        /// <summary>
        /// The geometry type.
        /// </summary>
        private int geometryType;

        /// <summary>
        /// The geometry layer.
        /// </summary>
        private OGR.Layer geometryLayer;

        /// <summary>
        /// The data source.
        /// </summary>
        private OGR.DataSource dataSource;

        /// <summary>
        /// Determine whether the provider is a temporary one or not. 
        /// If the provider is created by specifing the file name, isTemporary = true; otherwise, isTemprary = false.
        /// </summary>
        private bool isTemporary = false;

        #endregion

        /// <summary>
        /// Initializes a new instance of the <see cref="OgrShapeProvider&lt;T&gt;"/> class.
        /// </summary>
        /// <param name="geoLyr">The geometry layer.</param>
        /// <param name="dataSrc">The data source.</param>
        /// <param name="geoType">Type of the geometry <see cref="GeometryType"/>.</param>
        public OgrShapeProvider(OGR.Layer geoLyr, OGR.DataSource dataSrc, int geoType)
        {
            this.geometryLayer = geoLyr;
            this.dataSource = dataSrc;
            this.geometryType = geoType;
            this.layerName = geoLyr.GetName();
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="OgrShapeProvider&lt;T&gt;"/> class.
        /// </summary>
        public OgrShapeProvider()
        {
            // creates a temporary object and gets its type.
            T tmp = new T();
            this.geometryType = tmp.TypeFlag;

            // generates a file name.
            this.layerName = this.GenerateFileName();

            // creates the data source.
            this.dataSource = OgrShapeProvider<T>.CreateDataSet(this.layerName, this.layerName, "ESRI Shapefile", this.geometryType, out this.geometryLayer);

            // The file is deleted after this instance is disposed.
            this.isTemporary = true;
        }

        /// <summary>
        /// Releases unmanaged resources and performs other cleanup operations before the
        /// <see cref="OgrShapeProvider&lt;T&gt;"/> is reclaimed by garbage collection.
        /// </summary>
        ~OgrShapeProvider()
        {
            // If the files is created for temporary use, delete it when disposing this instance.
            if (File.Exists(this.layerName) && this.isTemporary)
            {
                File.Delete(this.layerName);
                File.Delete(this.layerName.Replace("shp", "shx"));
                File.Delete(this.layerName.Replace("shp", "dbf"));
            }

            // Releases the resources.
            if (this.dataSource != null)
            {
                this.Dispose();
            }
        }

        /// <summary>
        /// Gets the name of the geometry layer.
        /// </summary>
        /// <value>The name of the geometry layer.</value>
        public string LayerName
        {
            get { return this.layerName; }
        }

        /// <summary>
        /// Gets the type of the geometry.
        /// </summary>
        /// <value>The type of the geometry.</value>
        public int GeometryType
        {
            get { return this.geometryType; }
        }

        /// <summary>
        /// Loads the data source from a file.  
        /// </summary>
        /// <param name="filename">The filename.</param>
        /// <param name="dataType">Type of the geometry.</param>
        /// <returns>The data source created.</returns>
        public static OGR.DataSource ReadDataSet(string filename, ref int dataType)
        {
            return ReadDataSet(filename, false, ref dataType);
        }

        /// <summary>
        /// Loads the data source from a file.  
        /// </summary>
        /// <param name="filename">The filename.</param>
        /// <param name="readOnly">if set to <c>true</c>, the data is [read only].</param>
        /// <param name="dataType">Type of the data.</param>
        /// <returns>The data source created.</returns>
        public static OGR.DataSource ReadDataSet(string filename, bool readOnly, ref int dataType)
        {
            try
            {
                /* -------------------------------------------------------------------- */
                /*      Register driver(s).                                             */
                /* -------------------------------------------------------------------- */
                OGR.Ogr.RegisterAll();

                /* -------------------------------------------------------------------- */
                /*      Open dataset.                                                   */
                /* -------------------------------------------------------------------- */
                int update = readOnly ? 0 :1;
                OGR.DataSource ds = OGR.Ogr.Open(filename, update);

                if (ds == null)
                {
                    throw new Exception("Can't open " + filename);
                }

                /* -------------------------------------------------------------------- */
                /*      Get the data layer.                                                */
                /* -------------------------------------------------------------------- */
                OGR.Layer ly = ds.GetLayerByIndex(0);
                if (ly == null)
                {
                    throw new Exception("Get layer failed.");
                }

                // Gets a feature.
                OGR.Feature ft = ly.GetFeature(0);
                if (ft == null)
                {
                    throw new Exception("Get Feature failed.");
                }

                // Gets a geometry.
                OGR.Geometry geo = ft.GetGeometryRef();
                if (geo == null)
                {
                    throw new Exception("Get Geometry failed.");
                }

                // Gets the geometry type.
                dataType = (int)geo.GetGeometryType();

                ft.Dispose();
                geo.Dispose();

                return ds;
            }
            catch (Exception e)
            {
                throw new Exception("Application error: " + e.Message);
            }
        }

        /// <summary>
        /// Creates a data set.
        /// </summary>
        /// <param name="fileName">Name of the file.</param>
        /// <param name="layerName">Name of the layer.</param>
        /// <param name="drvName">Name of the driver.</param>
        /// <param name="dataType">Type of the geometry.</param>
        /// <param name="layer">The data layer.</param>
        /// <returns>The data source created.</returns>
        public static OGR.DataSource CreateDataSet(string fileName, string layerName, string drvName, int dataType, out OGR.Layer layer)
        {
            try
            {
                /* -------------------------------------------------------------------- */
                /*      Register driver(s).                                             */
                /* -------------------------------------------------------------------- */
                OGR.Ogr.RegisterAll();

                /* -------------------------------------------------------------------- */
                /*      Get driver                                                      */
                /* -------------------------------------------------------------------- */
                OGR.Driver drv = OGR.Ogr.GetDriverByName(drvName);

                if (drv == null)
                {
                    throw new Exception("Can't get driver.");
                }

                /* -------------------------------------------------------------------- */
                /*      Open dataset.                                                   */
                /* -------------------------------------------------------------------- */
                OGR.DataSource ds = drv.CreateDataSource(fileName, null);

                if (ds == null)
                {
                    throw new Exception("Can't open " + fileName);
                }

                // Create data layer.
                layer = ds.CreateLayer(layerName, null, (OGR.wkbGeometryType)dataType, null);
                if (layer == null)
                {
                    throw new Exception("Create layer failed.");
                }

                // Adds the "Name" field.
                OGR.FieldDefn fd = new OGR.FieldDefn("Name", OGR.FieldType.OFTString);
                if (layer.CreateField(fd, 0) != 0)
                {
                    throw new Exception("Create field failed.");
                }

                return ds;
            }
            catch (Exception e)
            {
                throw new Exception("Application ERROR: " + e.Message);
            }
        }

        /// <summary>
        /// This method must be called to close this data source.
        /// </summary>
        public void Dispose()
        {
            if (this.geometryLayer != null)
            {
                this.geometryLayer.Dispose();
                this.geometryLayer = null;
            }

            if (this.dataSource != null)
            {
                this.dataSource.Dispose();
                this.dataSource = null;
            }
        }

        /// <summary>
        /// Gets the bounding box.
        /// </summary>
        /// <returns>The bounding box.</returns>
        public BoundingBox GetExtent()
        {
            OGR.Envelope env = new OGR.Envelope();
            this.geometryLayer.GetExtent(env, 1);

            return new BoundingBox(env.MinX, env.MinY, env.MaxX, env.MaxY);
        }

        /// <summary>
        /// Gets the geometry at the specified index.
        /// </summary>
        /// <param name="index">The index of the geometry.</param>
        /// <returns>The geometry.</returns>
        private Feature GetGeometry(int index)
        {
            int fid = this.IndexToFid(this.deleteFid, index);
            OGR.Feature f = this.geometryLayer.GetFeature(fid);
            return this.GetGeometry(f);
        }

        /// <summary>
        /// Convertes the indexe to fid. Since the geometries can be deleted, the geometry at <c>index</c> does not have the fid equal to <c>index</c>.
        /// </summary>
        /// <param name="deleteFid">The delete fid. The fids are sorted in the order from small to large.</param>
        /// <param name="index">The index converted.</param>
        /// <returns>The corresponding fid.</returns>
        private int IndexToFid(List<int> deleteFid, int index)
        {
            int cnt = deleteFid.Count;
            if (cnt <= 0)
            {
                return index;
            }

            // the start, end and middle index.
            int idxS = 0, idxE = cnt - 1, idxM;

            /******************************************************************************
             * 
             * Example:
             *      All the fids:   0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
             *      Deleted fids:   0, 1, 2, 6, 8
             *      Result:
             *                      0   -   3
             *                      1   -   4
             *                      2   -   5
             *                      3   -   7
             *                      4   -   9
             *                      5   -   10
             * 
             ******************************************************************************/
            // Gets the samllest count that is large than (index + 1)
            while (idxS < idxE)
            {
                // Updates the middle index.
                idxM = (idxE - idxS) / 2 + idxS;

                // Updates the start and end index.
                if (index  < deleteFid[idxM] - idxM)
                {
                    idxE = idxM;
                }
                else
                {
                    idxS = idxM + 1;
                }
            }

            if (index  < deleteFid[idxE] - idxE)
            {
                return index + idxE;
            }
            else  // The index is larger than the largest deleted fid.
            {
                return index + idxE + 1;
            }
        }

        /// <summary>
        /// Gets the index of the by binary searching.
        /// </summary>
        /// <param name="values">The list of values.</param>
        /// <param name="match">The value to be searched.</param>
        /// <returns>The index of </returns>
        private int BinarySearch(List<int> values, int match)
        {
            int cnt = values.Count;
            if (cnt <= 0)
            {
                return 0;
            }

            // the start, end and middle index.
            int idxS = 0, idxE = cnt - 1, idxM;

            while (idxS < idxE)
            {
                idxM = (idxE - idxS) / 2 + idxS;

                if (match < values[idxM])
                {
                    idxE = idxM;
                }
                else
                {
                    idxS = idxM + 1;
                }
            }

            if (match < values[idxE])
            {
                return idxE;
            }
            else
            {
                return idxE + 1;
            }
        }

        /// <summary>
        /// Reads a feature <see cref="Feature"/> from an OGR feature.
        /// </summary>
        /// <param name="feature">The OGR feature.</param>
        /// <returns>The new feature.</returns>
        private Feature GetGeometry(OGR.Feature feature)
        {
            // Reads the geometry
            T geo = new T();
            OGR.Geometry ogrGeo = feature.GetGeometryRef();
            this.ReadGeometry(ogrGeo, geo);

            // Reads the attributes
            TagCollection attr = this.ReadAttributes(feature);

            // returns a new feature.
            return new Feature(geo, attr);               
        }

        /// <summary>
        /// Reads the points of an OGR geometry to a <see cref="Geometry"/>.
        /// </summary>
        /// <param name="ogrGeo">The OGR geometry.</param>
        /// <param name="geometry">The new geometry.</param>
        private void ReadGeometry(OGR.Geometry ogrGeo, Geometry geometry)
        {
            // Travesal the inner geometries
            int geoCnt = ogrGeo.GetGeometryCount();
            for (int i = 0; i < geoCnt; i++)
            {
                OGR.Geometry ogrInner = ogrGeo.GetGeometryRef(i);
                Geometry inner = geometry.CreateGeometry();
                this.ReadGeometry(ogrInner, inner);
            }

            // Adds the points.
            int cnt = ogrGeo.GetPointCount();
            for (int i = 0; i < cnt; i++)
            {
                geometry.AddPoint(new Point2d(ogrGeo.GetX(i), ogrGeo.GetY(i)));
            }
        }

        /// <summary>
        /// Reads the attributes.
        /// </summary>
        /// <param name="feature">The feature.</param>
        /// <returns>The collection of attributes.</returns>
        private TagCollection ReadAttributes(OGR.Feature feature)
        {
            TagCollection tags = new TagCollection("Attributes");

            int cnt = feature.GetFieldCount();
            for (int i = 0; i < cnt; i++)
            {
                using (OGR.FieldDefn fieldDef = feature.GetFieldDefnRef(i))
                {
                    string name = fieldDef.GetNameRef();
                    switch (fieldDef.GetFieldType())
                    {
                        //case FieldType.OFTBinary:
                        //    oCol.DataType = typeof(byte[]);
                        //    break;
                        case OGR.FieldType.OFTInteger:
                            tags.Add(new IntTag(name, feature.GetFieldAsInteger(i)));
                            break;
                        case OGR.FieldType.OFTReal:
                            tags.Add(new DoubleTag(name, feature.GetFieldAsDouble(i)));
                            break;
                        case OGR.FieldType.OFTString:
                            tags.Add(new StringTag(name, feature.GetFieldAsString(i)));
                            break;
                        default:
                            break;
                    }
                }
            }

            return tags;
        }

        /// <summary>
        /// method used to obtain features that match a certain spatial or attribute filter.
        /// </summary>
        /// <param name="bbox">The bounding box.y</param>
        /// <returns>The features insects with the bounding box</returns>
        public List<Feature> GetGeometries(BoundingBox bbox)
        {
            // resets
            this.geometryLayer.ResetReading();
            OGR.Geometry filter = this.geometryLayer.GetSpatialFilter();

            // sets the spatial filter rect.
            this.geometryLayer.SetSpatialFilterRect(bbox.Left, bbox.Bottom, bbox.Right, bbox.Top);

            // allocate buffer.
            List<Feature> ret = new List<Feature>(this.geometryLayer.GetFeatureCount(1));

            // reads the geometries.
            OGR.Feature ogrFea;
            while ((ogrFea = this.geometryLayer.GetNextFeature()) != null)
            {
                ret.Add(this.GetGeometry(ogrFea));
                ogrFea.Dispose();
            }

            // resets the layer and filter.
            this.geometryLayer.ResetReading();
            this.geometryLayer.SetSpatialFilter(filter);

            // returns.
            return ret;
        }

        /// <summary>
        /// Adds a geometry to the data source.
        /// </summary>
        /// <param name="feature">The feature.</param>
        private void AddGeometry(Feature feature)
        {
            try
            {
                // creates a feature.
                OGR.Feature ogrFea = new OGR.Feature(this.geometryLayer.GetLayerDefn());

                // creates the geometry.
                OGR.Geometry ogrGeo = new OGR.Geometry((OGR.wkbGeometryType)feature.Geometry.TypeFlag);
                this.WriteGeometry(ogrGeo, feature.Geometry);

                // sets the attributes.
                if (feature.Attributes != null)
                {
                    this.WriteAttributes(ogrFea, feature.Attributes);
                }

                // adds the feature.
                ogrFea.SetGeometry(ogrGeo);
                this.geometryLayer.CreateFeature(ogrFea);
            }
            catch (Exception ex)
            {
                throw new VectorException(ex.Message);
            }
        }

        /// <summary>
        /// Sets the geometry at the specifed index.
        /// </summary>
        /// <param name="index">The index.</param>
        /// <param name="feature">The feature.</param>
        private void SetGeometry(int index, Feature feature)
        {
            try
            {
                int fid = this.IndexToFid(this.deleteFid, index);

                // gets the feature.
                OGR.Feature ogrFea = this.geometryLayer.GetFeature(fid);

                // creates the geometry.
                OGR.Geometry ogrGeo = new OGR.Geometry((OGR.wkbGeometryType)this.geometryType);
                this.WriteGeometry(ogrGeo, feature.Geometry);

                // writes the attributes.
                if (feature.Attributes != null)
                {
                    this.WriteAttributes(ogrFea, feature.Attributes);
                }

                // adds the feature.
                ogrFea.SetGeometry(ogrGeo);
                this.geometryLayer.SetFeature(ogrFea);
            }
            catch (Exception ex)
            {
                throw new VectorException(ex.Message);
            }
        }

        /// <summary>
        /// Writes the points of a <see cref="Geometry"/> to a OGR geometry.
        /// </summary>
        /// <param name="ogrGeo">The OGR geometry.</param>
        /// <param name="geometry">The geometry to be written.</param>
        private void WriteGeometry(OGR.Geometry ogrGeo, Geometry geometry)
        {
            // Writes the inner geometries.
            int geoCnt = geometry.GeometryCount;
            for (int i = 0; i < geoCnt; i++)
            {
                Geometry inner = geometry.GetGeometry(i);
                OGR.Geometry ogrInner = new OGR.Geometry((OGR.wkbGeometryType)inner.TypeFlag);
                this.WriteGeometry(ogrInner, inner);
            }

            // Writes the points.
            int cnt = geometry.PointCount;
            for (int i = 0; i < cnt; i++)
            {
                Point2d p = geometry.GetPoint(i);
                ogrGeo.AddPoint_2D(p.X, p.Y);
            }
        }

        /// <summary>
        /// Writes the attributes.
        /// </summary>
        /// <param name="feature">The feature.</param>
        /// <param name="attributes">The attributes.</param>
        private void WriteAttributes(OGR.Feature feature, TagCollection attributes)
        {
            foreach (Tag t in attributes.Enumerate())
            {
                if (t.ValueType == typeof(int))
                {
                    IntTag itag = (IntTag)t;
                    feature.SetField(itag.Name, itag.Value);
                }
                else if (t.ValueType == typeof(double))
                {
                    DoubleTag dtag = (DoubleTag)t;
                    feature.SetField(dtag.Name, dtag.Value);
                }
                else if (t.ValueType == typeof(string))
                {
                    StringTag stag = (StringTag)t;
                    feature.SetField(stag.Name, stag.Value);
                }
            }
        }

        /// <summary>
        /// Deletes the geometry at the specified index.
        /// </summary>
        /// <param name="index">The index.</param>
        private void DeleteGeometryAt(int index)
        {
            try
            {
                // Deletes the feature.
                this.geometryLayer.ResetReading();
                int fid = this.IndexToFid(this.deleteFid, index);
                this.geometryLayer.DeleteFeature(fid);

                // Records the deleted fid.
                int pos = this.BinarySearch(this.deleteFid, fid);
                this.deleteFid.Insert(pos, fid);

                // Flushes the data set or not.
                if (this.deleteFid.Count >= OgrShapeProvider<T>.MaxDeleteCount)
                {
                    this.Flush();
                    this.deleteFid.Clear();
                }
            }
            catch (Exception ex)
            {
                throw new VectorException(ex.Message);
            }
        }

        /// <summary>
        /// Updates the data source.
        /// </summary>
        public void Flush()
        {
            try
            {
                this.geometryLayer.ResetReading();
                this.dataSource.ExecuteSQL("REPACK " + this.geometryLayer.GetName(), null, "");
            }
            catch (Exception ex)
            {
                throw new VectorException(ex.Message);
            }
        }

        /// <summary>
        /// Generates the name of the file.
        /// </summary>
        /// <returns>The file name.</returns>
        private string GenerateFileName()
        {
            if (!Directory.Exists("Cache"))
            {
                Directory.CreateDirectory("Cache");
                return OgrShapeProvider<T>.FilenamePrefix + "0.shp";
            }

            int index = 0;
            while (true)
            {
                string name = "Cache\\" + OgrShapeProvider<T>.FilenamePrefix + index.ToString() + ".shp";
                if (!File.Exists(name))
                {
                    return name;
                }
                else
                {
                    index++;
                }
            }
        }

        #region IShapeProvider Members

        /// <summary>
        /// Gets the count of geometries in the collection.
        /// </summary>
        /// <value>The number of geometries.</value>
        public int Count
        {
            get { return this.geometryLayer.GetFeatureCount(1) - this.deleteFid.Count; }
        }

        /// <summary>
        /// Gets or sets the feature at the specified index.
        /// </summary>
        /// <value></value>
        public Feature this[int index]
        {
            get
            {
                return this.GetGeometry(index);
            }
            set
            {
                this.SetGeometry(index, value);
            }
        }

        /// <summary>
        /// Adds a new feature to the collection.
        /// </summary>
        /// <param name="feature">The feature added.</param>
        public void Add(Feature feature)
        {
            this.AddGeometry(feature);
        }

        /// <summary>
        /// Adds an array of features to the collection.
        /// </summary>
        /// <param name="features">The features added.</param>
        public void Add(Feature[] features)
        {
            foreach (Feature f in features)
            {
                this.AddGeometry(f);
            }
        }

        /// <summary>
        /// Adds a list of features to the collection.
        /// </summary>
        /// <param name="features">The features added.</param>
        public void Add(List<Feature> features)
        {
            foreach (Feature f in features)
            {
                this.AddGeometry(f);
            }
        }

        /// <summary>
        /// Removes the specified feature.
        /// </summary>
        /// <param name="feature">The feature removed.</param>
        public void Remove(Feature feature)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Removes the geometry at the specifed index.
        /// </summary>
        /// <param name="index">The index.</param>
        public void RemoveAt(int index)
        {
            this.DeleteGeometryAt(index);
        }

        /// <summary>
        /// Clears all features in this provider.
        /// </summary>
        public void Clear()
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Gets the geometries within the bounding box.
        /// </summary>
        /// <param name="bbox">The bounding box.</param>
        /// <returns>A list of geometries.</returns>
        public List<Feature> GetFeatures(BoundingBox bbox)
        {
            return this.GetGeometries(bbox);
        }

        /// <summary>
        /// Gets the enumerator of all features.
        /// </summary>
        /// <returns>The enumerator to access the features.</returns>
        public IEnumerable<Feature> GetFeatures()
        {
            throw new NotImplementedException();
        }

        #endregion
    }
}
